/*-------------------------------------------------------------------------
 *
 * nodeLimit.c
 *	  处理适当限制查询结果的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/executor/nodeLimit.c
 *
 *-------------------------------------------------------------------------
 */
/*
 * 接口例程
 *		ExecLimit		- 提取有限范围的元组
 *		ExecInitLimit	- 初始化节点和子节点..
 *		ExecEndLimit	- 关闭节点和子节点
 */

#include "postgres.h"

#include "executor/executor.h"
#include "executor/nodeLimit.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"

static void fc_recompute_limits(LimitState *fc_node);
static int64 fc_compute_tuples_needed(LimitState *fc_node);


/* ----------------------------------------------------------------
 *		ExecLimit
 *
 *		这是一个非常简单的节点，仅对返回的元组流执行 LIMIT/OFFSET
 *		过滤，来自子计划。
 * ----------------------------------------------------------------
 */
static TupleTableSlot *			/* 返回：一个元组或 NULL */
fc_ExecLimit(PlanState *fc_pstate)
{
	LimitState *fc_node = castNode(LimitState, fc_pstate);
	ExprContext *fc_econtext = fc_node->ps.ps_ExprContext;
	ScanDirection fc_direction;
	TupleTableSlot *fc_slot;
	PlanState  *fc_outerPlan;

	CHECK_FOR_INTERRUPTS();

	/*
	 * 从节点获取信息
	 */
	fc_direction = fc_node->ps.state->es_direction;
	fc_outerPlan = outerPlanState(fc_node);

	/*
	 * 主要逻辑是一个简单的状态机。
	 */
	switch (fc_node->lstate)
	{
		case LIMIT_INITIAL:

			/*
			 * 针对这个节点的第一次调用，因此需要计算 limit/offset。 （因为在 ExecInitLimit 期间，上层节点的参数不会被设置，所以不能更早进行此操作。）这也将 position 设置为 0，并将状态更改为 LIMIT_RESCAN。
			 */
			fc_recompute_limits(fc_node);

			/* FALL THRU */

		case LIMIT_RESCAN:

			/*
			 * 如果是向后扫描，则仅返回 NULL，而不更改状态。
			 */
			if (!ScanDirectionIsForward(fc_direction))
				return NULL;

			/*
			 * 检查窗口是否为空；如果是，则将其视为空子计划。
			 */
			if (fc_node->count <= 0 && !fc_node->noCount)
			{
				fc_node->lstate = LIMIT_EMPTY;
				return NULL;
			}

			/*
			 * 从子计划中获取行，直到我们达到 position > offset。
			 */
			for (;;)
			{
				fc_slot = ExecProcNode(fc_outerPlan);
				if (TupIsNull(fc_slot))
				{
					/*
					 * 子计划返回的元组太少，以至于我们无法产生任何输出。
					 */
					fc_node->lstate = LIMIT_EMPTY;
					return NULL;
				}

				/*
				 * 在限制条件下的元组是后续执行中进行比较以检测平局所需的。
				 */
				if (fc_node->limitOption == LIMIT_OPTION_WITH_TIES &&
					fc_node->position - fc_node->offset == fc_node->count - 1)
				{
					ExecCopySlot(fc_node->last_slot, fc_slot);
				}
				fc_node->subSlot = fc_slot;
				if (++fc_node->position > fc_node->offset)
					break;
			}

			/*
			 * 好吧，我们得到了窗口的第一个元组。
			 */
			fc_node->lstate = LIMIT_INWINDOW;
			break;

		case LIMIT_EMPTY:

			/*
			 * 已知子计划不返回任何元组（通常不超过 OFFSET 个元组）。因此我们不返回任何元组。
			 */
			return NULL;

		case LIMIT_INWINDOW:
			if (ScanDirectionIsForward(fc_direction))
			{
				/*
				 * 向前扫描，因此检查是否已超出窗口的末端。在窗口的末尾，行为取决于是否指定了 WITH TIES：如果指定了，我们需要将状态机更改为 WINDOWEND_TIES，并继续执行该情况的代码。如果没有（没有指定任何内容，或者仅指定了 ONLY），则返回 NULL，而不推进子计划或 position 变量，但是需要更改状态机以记录这一点。
				 *
				 * 一旦到达末尾，理想情况下，我们将关闭并行资源；但这将破坏可能需要用于重新扫描的并行上下文。为了做到这一点，我们需要找到一种方法来传递更多有关重新扫描是否可能的信息。
				 */
				if (!fc_node->noCount &&
					fc_node->position - fc_node->offset >= fc_node->count)
				{
					if (fc_node->limitOption == LIMIT_OPTION_COUNT)
					{
						fc_node->lstate = LIMIT_WINDOWEND;
						return NULL;
					}
					else
					{
						fc_node->lstate = LIMIT_WINDOWEND_TIES;
						/* 我们将继续执行下一个案例 */
					}
				}
				else
				{
					/*
					 * 从子计划中获取下一个元组（如果有）。
					 */
					fc_slot = ExecProcNode(fc_outerPlan);
					if (TupIsNull(fc_slot))
					{
						fc_node->lstate = LIMIT_SUBPLANEOF;
						return NULL;
					}

					/*
					 * 如果 WITH TIES 处于活动状态，并且这是窗口内的最后一个元组，则将其保存以供后续 WINDOWEND_TIES 处理使用。
					 */
					if (fc_node->limitOption == LIMIT_OPTION_WITH_TIES &&
						fc_node->position - fc_node->offset == fc_node->count - 1)
					{
						ExecCopySlot(fc_node->last_slot, fc_slot);
					}
					fc_node->subSlot = fc_slot;
					fc_node->position++;
					break;
				}
			}
			else
			{
				/*
				 * 向后扫描，因此检查是否已超出窗口的起点。与上面相同，只有在这种情况下才更改状态机的状态。
				 */
				if (fc_node->position <= fc_node->offset + 1)
				{
					fc_node->lstate = LIMIT_WINDOWSTART;
					return NULL;
				}

				/*
				 * 从子计划中获取前一个元组；应该有一个！
				 */
				fc_slot = ExecProcNode(fc_outerPlan);
				if (TupIsNull(fc_slot))
					elog(ERROR, "LIMIT subplan failed to run backwards");
				fc_node->subSlot = fc_slot;
				fc_node->position--;
				break;
			}

			Assert(fc_node->lstate == LIMIT_WINDOWEND_TIES);
			/* FALL THRU */

		case LIMIT_WINDOWEND_TIES:
			if (ScanDirectionIsForward(fc_direction))
			{
				/*
				 * 推进子计划，直到找到第一个具有不同 ORDER BY 路径键的行。
				 */
				fc_slot = ExecProcNode(fc_outerPlan);
				if (TupIsNull(fc_slot))
				{
					fc_node->lstate = LIMIT_SUBPLANEOF;
					return NULL;
				}

				/*
				 * 测试新元组和最后一个元组是否匹配。如果匹配，我们返回该元组。
				 */
				fc_econtext->ecxt_innertuple = fc_slot;
				fc_econtext->ecxt_outertuple = fc_node->last_slot;
				if (ExecQualAndReset(fc_node->eqfunction, fc_econtext))
				{
					fc_node->subSlot = fc_slot;
					fc_node->position++;
				}
				else
				{
					fc_node->lstate = LIMIT_WINDOWEND;
					return NULL;
				}
			}
			else
			{
				/*
				 * 向后扫描，因此检查是否已超出窗口的起点。仅在这种情况下更改状态机的状态。
				 */
				if (fc_node->position <= fc_node->offset + 1)
				{
					fc_node->lstate = LIMIT_WINDOWSTART;
					return NULL;
				}

				/*
				 * 从子计划中获取前一个元组；应该有一个！并更改状态机的状态。
				 */
				fc_slot = ExecProcNode(fc_outerPlan);
				if (TupIsNull(fc_slot))
					elog(ERROR, "LIMIT subplan failed to run backwards");
				fc_node->subSlot = fc_slot;
				fc_node->position--;
				fc_node->lstate = LIMIT_INWINDOW;
			}
			break;

		case LIMIT_SUBPLANEOF:
			if (ScanDirectionIsForward(fc_direction))
				return NULL;

			/*
			 * 从子计划 EOF 撤回，因此重新获取前一个元组；应该有一个！注意，前一个元组必须在窗口内。
			 */
			fc_slot = ExecProcNode(fc_outerPlan);
			if (TupIsNull(fc_slot))
				elog(ERROR, "LIMIT subplan failed to run backwards");
			fc_node->subSlot = fc_slot;
			fc_node->lstate = LIMIT_INWINDOW;
			/* position 不会改变，因为我们之前没有推进它 */
			break;

		case LIMIT_WINDOWEND:
			if (ScanDirectionIsForward(fc_direction))
				return NULL;

			/*
			 * 我们已经超越了一个位置来检测平局，因此重新获取前一个元组；应该有一个！注意，前一个元组必须在窗口内。
			 */
			if (fc_node->limitOption == LIMIT_OPTION_WITH_TIES)
			{
				fc_slot = ExecProcNode(fc_outerPlan);
				if (TupIsNull(fc_slot))
					elog(ERROR, "LIMIT subplan failed to run backwards");
				fc_node->subSlot = fc_slot;
				fc_node->lstate = LIMIT_INWINDOW;
			}
			else
			{
				/*
				 * 从窗口末尾撤回：简单地重新返回从子计划中获取的最后一个元组。
				 */
				fc_slot = fc_node->subSlot;
				fc_node->lstate = LIMIT_INWINDOW;
				/* position 不会改变，因为我们之前没有推进它 */
			}
			break;

		case LIMIT_WINDOWSTART:
			if (!ScanDirectionIsForward(fc_direction))
				return NULL;

			/*
			 * 在从窗口起点撤回后推进：简单地重新返回从子计划中获取的最后一个元组。
			 */
			fc_slot = fc_node->subSlot;
			fc_node->lstate = LIMIT_INWINDOW;
			/* position 不会改变，因为我们之前没有改变它 */
			break;

		default:
			elog(ERROR, "impossible LIMIT state: %d",
				 (int) fc_node->lstate);
			fc_slot = NULL;		/* 保持编译器安静 */
			break;
	}

	/* 返回当前元组 */
	Assert(!TupIsNull(fc_slot));

	return fc_slot;
}

/*
 * 计算 limit/offset 表达式 —— 在启动或重新扫描时完成。
 *
 * 这也是重置当前状态信息的便捷位置。
 */
static void fc_recompute_limits(LimitState *fc_node)
{
	ExprContext *fc_econtext = fc_node->ps.ps_ExprContext;
	Datum		fc_val;
	bool		fc_isNull;

	if (fc_node->limitOffset)
	{
		fc_val = ExecEvalExprSwitchContext(fc_node->limitOffset,
										fc_econtext,
										&fc_isNull);
		/* 将 NULL offset 视为无 offset */
		if (fc_isNull)
			fc_node->offset = 0;
		else
		{
			fc_node->offset = DatumGetInt64(fc_val);
			if (fc_node->offset < 0)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE),
						 errmsg("OFFSET must not be negative")));
		}
	}
	else
	{
		/* 未提供 OFFSET */
		fc_node->offset = 0;
	}

	if (fc_node->limitCount)
	{
		fc_val = ExecEvalExprSwitchContext(fc_node->limitCount,
										fc_econtext,
										&fc_isNull);
		/* 将 NULL count 视为无 count（LIMIT ALL） */
		if (fc_isNull)
		{
			fc_node->count = 0;
			fc_node->noCount = true;
		}
		else
		{
			fc_node->count = DatumGetInt64(fc_val);
			if (fc_node->count < 0)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_ROW_COUNT_IN_LIMIT_CLAUSE),
						 errmsg("LIMIT must not be negative")));
			fc_node->noCount = false;
		}
	}
	else
	{
		/* 没有提供 COUNT */
		fc_node->count = 0;
		fc_node->noCount = true;
	}

	/* 重置位置到扫描开始 */
	fc_node->position = 0;
	fc_node->subSlot = NULL;

	/* 设置状态机状态 */
	fc_node->lstate = LIMIT_RESCAN;

	/*
	 * 通知子节点关于限制。注意：不要通过跳过 ExecSetTupleBound 来“优化”，
	 * 如果 compute_tuples_needed 返回 < 0。我们必须无论如何更新子节点，
	 * 以防这是重新扫描，并且上次我们得到不同的结果。
	 */
	ExecSetTupleBound(fc_compute_tuples_needed(fc_node), outerPlanState(fc_node));
}

/*
 * 计算满足此 Limit 节点所需的最大元组数量。
 * 如果没有可确定的限制，则返回负值。
 */
static int64 fc_compute_tuples_needed(LimitState *fc_node)
{
	if ((fc_node->noCount) || (fc_node->limitOption == LIMIT_OPTION_WITH_TIES))
		return -1;
	/* 注意：如果此值溢出，我们将返回负值，这没问题 */
	return fc_node->count + fc_node->offset;
}

/* ----------------------------------------------------------------
 *		ExecInitLimit
 *
 *		这初始化了 limit 节点状态结构和
 *		节点的子计划。
 * ----------------------------------------------------------------
 */
LimitState *
ExecInitLimit(Limit *fc_node, EState *fc_estate, int fc_eflags)
{
	LimitState *fc_limitstate;
	Plan	   *fc_outerPlan;

	/* 检查不支持的标志 */
	Assert(!(fc_eflags & EXEC_FLAG_MARK));

	/*
	 * 创建状态结构
	 */
	fc_limitstate = makeNode(LimitState);
	fc_limitstate->ps.plan = (Plan *) fc_node;
	fc_limitstate->ps.state = fc_estate;
	fc_limitstate->ps.ExecProcNode = fc_ExecLimit;

	fc_limitstate->lstate = LIMIT_INITIAL;

	/*
	 * 杂项初始化
	 *
	 * Limit 节点从不调用 ExecQual 或 ExecProject，但他们仍然需要一个
	 * exprcontext 来评估 limit/offset 参数。
	 */
	ExecAssignExprContext(fc_estate, &fc_limitstate->ps);

	/*
	 * 初始化外部计划
	 */
	fc_outerPlan = outerPlan(fc_node);
	outerPlanState(fc_limitstate) = ExecInitNode(fc_outerPlan, fc_estate, fc_eflags);

	/*
	 * 初始化子表达式
	 */
	fc_limitstate->limitOffset = ExecInitExpr((Expr *) fc_node->limitOffset,
										   (PlanState *) fc_limitstate);
	fc_limitstate->limitCount = ExecInitExpr((Expr *) fc_node->limitCount,
										  (PlanState *) fc_limitstate);
	fc_limitstate->limitOption = fc_node->limitOption;

	/*
	 * 初始化结果类型。
	 */
	ExecInitResultTypeTL(&fc_limitstate->ps);

	fc_limitstate->ps.resultopsset = true;
	fc_limitstate->ps.resultops = ExecGetResultSlotOps(outerPlanState(fc_limitstate),
													&fc_limitstate->ps.resultopsfixed);

	/*
	 * limit 节点不执行投影，因此为此节点适当地初始化投影信息
	 */
	fc_limitstate->ps.ps_ProjInfo = NULL;

	/*
	 * 初始化相等性评估，以检测平局。
	 */
	if (fc_node->limitOption == LIMIT_OPTION_WITH_TIES)
	{
		TupleDesc	fc_desc;
		const TupleTableSlotOps *fc_ops;

		fc_desc = ExecGetResultType(outerPlanState(fc_limitstate));
		fc_ops = ExecGetResultSlotOps(outerPlanState(fc_limitstate), NULL);

		fc_limitstate->last_slot = ExecInitExtraTupleSlot(fc_estate, fc_desc, fc_ops);
		fc_limitstate->eqfunction = execTuplesMatchPrepare(fc_desc,
														fc_node->uniqNumCols,
														fc_node->uniqColIdx,
														fc_node->uniqOperators,
														fc_node->uniqCollations,
														&fc_limitstate->ps);
	}

	return fc_limitstate;
}

/* ----------------------------------------------------------------
 *		ExecEndLimit
 *
 *		这关闭子计划并释放分配给此节点的资源。
 * ----------------------------------------------------------------
 */
void ExecEndLimit(LimitState *fc_node)
{
	ExecFreeExprContext(&fc_node->ps);
	ExecEndNode(outerPlanState(fc_node));
}


void ExecReScanLimit(LimitState *fc_node)
{
	/*
	 * 如果参数发生变化，则重新计算 limit/offset，并重置状态机。
	 * 我们必须在重新扫描子节点之前执行此操作，以防它是一个 Sort，
	 * 我们将参数传递给它。
	 */
	fc_recompute_limits(fc_node);

	/*
	 * 如果子节点的chgParam不为空，则计划将由第一个ExecProcNode重新扫描。
	 */
	if (fc_node->ps.lefttree->chgParam == NULL)
		ExecReScan(fc_node->ps.lefttree);
}
